package com.quectel.dms.service.aiotap;

import com.fasterxml.jackson.databind.JsonNode;
import com.quectel.business.dms.AccessorUtils;
import com.quectel.business.dms.application.access.accessor.ipg.AbstractIntelligentParkingGateAccessor;
import com.quectel.constant.core.car.CarConstants;
import com.quectel.constant.core.eventrecord.CarAccessRecordConstants;
import com.quectel.constant.global.SystemConstants;
import com.quectel.core.module.device.dto.DeviceDto;
import com.quectel.core.module.device.service.DeviceService;
import com.quectel.util.common.JacksonUtils;
import com.quectel.util.common.MyFileUtils;
import com.quectel.util.kit.StorageKit;
import com.quectel.util.redis.RedisUtils;
import com.quectel.util.thread.ThreadPoolUtils;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.ptr.PointerByReference;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.*;

/**
 * 在这里编写说明
 *
 * @author: ryan
 * @email: ryan.ran@quectel.com
 * @date: 2023-08-17 10:40:00
 */

@Component
@Slf4j
public class AiotapService implements DisposableBean {

    /**
     * 连接设备 cache
     */
    private static final Map<Pointer, DeviceHandleCls> DEV_POOL = new ConcurrentHashMap<>();
    /**
     * 服务端口
     */
    private static final String ACTIVE_SVR_URL = "tcp://0.0.0.0:5555";

    private static final String AIOTAP_CAMERA_LOGIN_LOCK_KEY = "aiotap_camera_login_lock:";
    private static final String AIOTAP_ALARM_REPEAT_KEY = "aiotap_alarm_repeat_key:";


    private static final String PREFIX = "active://";

    /**
     * 事件订阅线程池
     */
    private static final ThreadPoolExecutor AIOTAP_ALARM_SUB_THREAD_POOL = ThreadPoolUtils.createCommonIOBoundPool("aiotap_alarm_sub_thread_pool_");

    private static final DelayQueue<DelayElement<Pointer>> SUB_DELAY_ELEMENTS = new DelayQueue<>();

    private static class DelayElement<E> implements Delayed {
        private final long expireTime;
        private final E e;

        public DelayElement(E e, long delay) {
            this.expireTime = System.currentTimeMillis() + delay;
            this.e = e;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            //判断是否过期
            return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(@NotNull Delayed delayed) {
            //排序规则：按照过期时间排序
            DelayElement that = (DelayElement) delayed;
            if (this.expireTime < that.expireTime) {
                return -1;
            } else if (this.expireTime > that.expireTime) {
                return 1;
            } else {
                return 0;
            }
        }

        public E getData() {
            return e;
        }
    }

    @Override
    public void destroy() throws Exception {
        ThreadPoolUtils.shutdown(AIOTAP_ALARM_SUB_THREAD_POOL);
    }

    @DubboReference
    private DeviceService deviceService;

    public void init (){
        PointerByReference outJsonPtr = new PointerByReference();


        int ret = AiotapLibrary.INSTANTCE.AIOTAP_GetSdkInfo(outJsonPtr);
        if (ret != AiotapError.ERROR_OK.getCode())
        {
            log.error("Aiotap Get sdk info fail");
            return ;
        }

        AiotapLibrary.INSTANTCE.AIOTAP_Free(outJsonPtr.getValue());

        AiotapLibrary.ConnInitParamStruct initParam = new AiotapLibrary.ConnInitParamStruct();
        initParam.cliStatusCb = (handle, status, urlOrSsid) -> {
            /**
             * 设备状态改变回调，dashboard 设置改变设备 sn 号，会触发该回调 status == 2 新设备号会触发 login 回调 status == 0
             * 如果直接断电拔网线，触发该回调 status == 1，此时不取消订阅，因为再次插网线的时候，会触发该回调 status == 0，但是不触发 login 回调
             * （订阅在 login 回调完成）
             */
            log.info("===========状态变更 start=================");
            log.info(urlOrSsid);
            log.info(status+"");
            log.info(handle+"");
            log.info("===========状态变更 end===================");

            // 更新状态
            String sn = urlOrSsid.substring(PREFIX.length());
            List<DeviceDto> deviceDtos = deviceService.selectCacheByUniqueId(sn);
            if (status == AiotapLibrary.AIOT_DEVCIE_STATUS_LOGOUT || status == AiotapLibrary.AIOT_DEVCIE_STATUS_DISCONNECT) {
                for (DeviceDto deviceDto : deviceDtos) {
                    DeviceDto updateObj = new DeviceDto();
                    updateObj.setId(deviceDto.getId());
                    updateObj.setOnlineStatus(SystemConstants.NO);
                    deviceService.updateById(updateObj);
                }
                if (status == AiotapLibrary.AIOT_DEVCIE_STATUS_LOGOUT) {
                    cancelSub(handle, DEV_POOL.get(handle));
                    DEV_POOL.remove(handle);
                }
            } else if (status == AiotapLibrary.AIOT_DEVCIE_STATUS_CONNECT) {
                for (DeviceDto deviceDto : deviceDtos) {
                    DeviceDto updateObj = new DeviceDto();
                    updateObj.setId(deviceDto.getId());
                    updateObj.setOnlineStatus(SystemConstants.YES);
                    deviceService.updateById(updateObj);
                }

                DeviceHandleCls deviceHandleCls = new DeviceHandleCls();
                deviceHandleCls.setDeviceDtoList(deviceDtos);
                DEV_POOL.put(handle, deviceHandleCls);
                // 订阅
                sub(handle);
            }
        };


        initParam.comFree = p -> {
            log.info("释放:"+p);
            long peer = Pointer.nativeValue(p);
            Native.free(peer);
        };

        Map<String, Object> paramJson = new HashMap<>();
        paramJson.put("active_server_url", ACTIVE_SVR_URL);
        initParam.param = JacksonUtils.toJsonString(paramJson);
        initParam.regCheckCb = (handle, url, activeParam, outParam) -> {
            log.debug("device map size : {}", DEV_POOL.size() );
            Boolean setnx = RedisUtils.setnx(AIOTAP_CAMERA_LOGIN_LOCK_KEY + url, url, 5, TimeUnit.MINUTES);
            if (Boolean.TRUE.equals(setnx)) {
                try {
                    if (DEV_POOL.containsKey(handle)) {
                        AiotapLibrary.INSTANTCE.AIOTAP_Logout(handle);
                        return 0;
                    }
                    JsonNode inJson = JacksonUtils.parse(activeParam);
                    String sn = inJson.get("sn").asText();
                    log.info("Aiotap D3X Camera login, sn: {}", sn);
                    List<DeviceDto> deviceDtos =
                            deviceService.selectCacheByUniqueId(sn);
                    if (CollectionUtils.isEmpty(deviceDtos)) {
                        log.warn("Aiotap D3X Camera login fail, sn: {}, because unregistered", sn);
                        AiotapLibrary.INSTANTCE.AIOTAP_Logout(handle);
                        return 0;
                    }
                    return 1;
                } finally {
                    RedisUtils.del(AIOTAP_CAMERA_LOGIN_LOCK_KEY + url);
                }
            }
            return 0;
        };

        ret = AiotapLibrary.INSTANTCE.AIOTAP_Init(initParam);
        log.info("Aiotap server start {}", AiotapError.ERROR_OK.getCode() == ret ? "success" : "fail");

    }

    private void sub(Pointer handle) {


        log.info("重新订阅" + handle);

        AlarmCb alarmCb = new AlarmCb();
        AlarmCbUsrArg alarmUserArg = new AlarmCbUsrArg();

        PointerByReference outJsonPtr = new PointerByReference();

        // 订阅流
        alarmUserArg.setHandle(handle);
        alarmUserArg.write();
        int subRet = AiotapLibrary.INSTANTCE.AIOTAP_DeviceAlarm_SubscribeStream(handle,
                outJsonPtr,
                alarmCb,
                alarmUserArg.getPointer(),
                0);
        String outJsonStr = checkGetResult(subRet, outJsonPtr);
        JsonNode outJson = JacksonUtils.parse(outJsonStr);
        int streamHandle = outJson.get("handle").intValue();

        // 订阅告警类型
        Map<String, Object> inJson = new HashMap<>();
        inJson.put("alarm_type", alarmTypeJson());
        inJson.put("handle", streamHandle);
        int subTypeRet = AiotapLibrary.INSTANTCE.AIOTAP_DeviceAlarm_SubscribeAlarmType(handle, JacksonUtils.toJsonString(inJson), 0);
        assert AiotapError.ERROR_OK.getCode() == subTypeRet;

        DEV_POOL.computeIfPresent(handle, (k, v) -> {
            v.setHandleStream(streamHandle);
            return v;
        });


    }

    private void cancelSub(Pointer handle, DeviceHandleCls deviceHandleCls) {
        if (deviceHandleCls != null && deviceHandleCls.getHandleStream() != null) {
            Map<String, Object> inJson = new HashMap<>();
            inJson.put("handle", deviceHandleCls.getHandleStream());
            AiotapLibrary.INSTANTCE.AIOTAP_DeviceAlarm_UnsubscribeStream(handle, JacksonUtils.toJsonString(inJson), 0);
        }
    }

    private String checkGetResult(int ret, PointerByReference outJsonPtr) {
        assert AiotapError.ERROR_OK.getCode() == ret;
        Pointer ptr = outJsonPtr.getValue();
        assert ptr != null;
        String outJson = ptr.getString(0);
        AiotapLibrary.INSTANTCE.AIOTAP_Free(ptr);
        return outJson;
    }

    private static List<Map<String, Object>> alarmTypeJson(){

        List<Map<String, Object>> alarmType = new ArrayList<>();
        Map<String, Object> alarmTypeItemB4H = new HashMap<>();
        Map<String, Object> alarmTypeItemIpc = new HashMap<>();
        List<String> minorTypeB4H = new ArrayList<>();
        List<String> minorTypeIpc = new ArrayList<>();

        minorTypeB4H.add("face");
        minorTypeB4H.add("pedestrian");
        minorTypeB4H.add("vehicle");
        minorTypeB4H.add("non_motor");
        alarmTypeItemB4H.put("major_type", "structure");
        alarmTypeItemB4H.put("minor_type", minorTypeB4H);
        alarmType.add(alarmTypeItemB4H);

        minorTypeIpc.add("face_capture");
        minorTypeIpc.add("body_capture");
        minorTypeIpc.add("face_comparison_successful");
        minorTypeIpc.add("stranger");
        minorTypeIpc.add("update_feature");
        alarmTypeItemIpc.put("major_type", "face_basic_business");
        alarmTypeItemIpc.put("minor_type", minorTypeIpc);
        alarmType.add(alarmTypeItemIpc);
        return alarmType;
    }

    @Data
    private class DeviceHandleCls {
        private List<DeviceDto> deviceDtoList;
//        private Pointer handle;
        private Integer handleStream;
    }

    public static class AlarmCb implements AiotapLibrary.AlarmCb {
        @Override
        public void invoke(AiotapLibrary.AlarmMsg.ByReference alarmMsg, Pointer userArg) {
            if (userArg == null) {
                return ;
            }
            AlarmCbUsrArg alarmUserArg = new AlarmCbUsrArg(userArg);
            alarmUserArg.read();

            DeviceHandleCls deviceHandleCls = DEV_POOL.get(alarmUserArg.getHandle());
            if (deviceHandleCls == null) {
                log.error("Aiotap device connect cache not exist.alarm event receive fail.");
                return ;
            }

            Boolean setnx = RedisUtils.setnx(AIOTAP_ALARM_REPEAT_KEY + deviceHandleCls.getHandleStream(), deviceHandleCls.getHandleStream() + "", 1, TimeUnit.MINUTES);
            if (Boolean.FALSE.equals(setnx)) {
                return ;
            }

            final Date current = new Date();

            // 去重后，开始拿数据
            byte[] info = alarmMsg.jsonInfo.getByteArray(0, alarmMsg.infoLen);
            String infoStr = new String(info);
            JsonNode alarmEventJson = JacksonUtils.parse(infoStr);
            String plateNo = alarmEventJson.get("plates").get(0).get("plate_no").asText();
            String url = null;

            AiotapLibrary.BinDataInfo[] binDataInfos = (AiotapLibrary.BinDataInfo[])alarmMsg.data.binDataInfos.toArray(alarmMsg.data.binDataInfoNum);
            for (AiotapLibrary.BinDataInfo binDataInfo : binDataInfos){
                byte[] data = binDataInfo.binData.getByteArray(0, binDataInfo.size);
                try {
                    InputStream inputStream = new ByteArrayInputStream(data);

                    File tempFile = MyFileUtils.writeFileToTempDir(inputStream, plateNo, false);
                    url = StorageKit.uploadToPublicBucketObtainUrl(tempFile);
                    if (StringUtils.isNotBlank(url)) {
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            for (DeviceDto deviceDto : deviceHandleCls.getDeviceDtoList()) {
                AbstractIntelligentParkingGateAccessor intelligentParkingGateAccessor = AccessorUtils.getIntelligentParkingGateAccessor(deviceDto.getDeviceType());
                assert intelligentParkingGateAccessor != null;

                AbstractIntelligentParkingGateAccessor.HandleReq handleReq = new AbstractIntelligentParkingGateAccessor
                        .HandleReq(deviceDto, CarConstants.LicenseColorEnum.COMMON, plateNo, CarAccessRecordConstants.InOrOutEnum.IN, current);
                handleReq.setSnapImages(JacksonUtils.toJsonString(Collections.singletonList(url)));
                intelligentParkingGateAccessor.accessHandle(handleReq);
            }
        }
    }

    @Data
    public static class AlarmCbUsrArg extends Structure {
        public Pointer handle;

        public AlarmCbUsrArg() {
            super();
        }

        public AlarmCbUsrArg(Pointer p) {
            super(p);
        }

        @Override
        protected List<String> getFieldOrder() {
            return Collections.singletonList("handle");
        }

        public static class ByReference extends AlarmCbUsrArg implements Structure.ByReference {
        }

        public static class ByValue extends AlarmCbUsrArg implements Structure.ByValue {
        }
    }

}
